useEffect
🧑🏻💻 useEffect
useEffect
는 컴포넌트를 외부 시스템과 동기화할 수 있는 React 훅이다.
✅ useEffect 문법
useEffect(setup, dependencies?)
setup
은 Effect의 로직이 포함된 함수이다.dependencies
는 setup 코드 내에서 참조된 모든 반응형 값의 목록이다.useEffect
는undefined
를 반환한다.
🧑🏻💻 알고 가기
✅ useEffect를 언제 사용해야 하는가?
setInterval()
및clearInterval()
로 관리되는 타이머 사용 시dialog.showModal()
및dialog.close()
로 관리되는 dialog 사용 시window.addEventListener()
및window.removeEventListener()
를 사용하는 이벤트를 구독할 때animation.start()
및animation.reset()
과 같은 API가 있는 타사 애니메이션 라이브러리를 사용할 때
✅ setup 함수와 cleanup 함수 동작
React는 컴포넌트가 DOM에 추가되면 setup 함수를 실행한다.
React는 (cleanup 함수가 있는 경우) 먼저 이전 값으로 cleanup 함수를 실행한 다음, 새 값으로 setup 함수를 실행한다.
컴포넌트가 DOM에서 제거되면, React는 마지막으로 cleanup 함수를 실행합니다.
✅ 전달된 dependencies에 따라 다른 Effect 동작
dependencies에 의존성 배열을 전달하면, 초기 렌더링 이후와 컴포넌트의 props나 state가 변경될 때 Effect가 실행된다.
dependencies에 빈 배열을 전달하면, 초기 렌더링 이후에만 Effect가 실행되고 컴포넌트의 props나 state가 변경되어도 Effect가 실행되지 않는다.
dependencies에 의존성을 전혀 전달하지 않으면, 컴포넌트를 (리)렌더링할 때마다 Effect가 다시 실행돤다.
참고로 의존성 배열에 전달되는 반응형 값에는 props와 state, 컴포넌트 내부에서 직접 선언된 모든 변수, 함수가 포함된다.
✅ 주의 사항
Effects는 서버 렌더링 중에는 실행되지 않고 클라이언트에서만 실행된다.
외부 시스템과 동기화하려는 목적이 아니라면 Effect가 필요하지 않을 수도 있다.
렌더링을 위해 데이터를 변환하는 경우 useEffect는 필요하지 않다.
의존성에 컴포넌트 내부에 정의된 객체 또는 함수가 있는 경우 렌더링이 진행될 때마다 새로 선언되기 때문에, Effect가 필요 이상으로 다시 실행될 수 있다.
Effect가 상호작용(예: 클릭) 으로 인한 것이 아니라면, React는 브라우저가 업데이트된 화면을 먼저 그리고 Effect를 실행하도록 한다.
상호작용으로 인해 state 변화로 Effect가 발생하더라도, 브라우저가 화면을 다시 칠하지 못하도록 차단할 경우에는
useLayoutEffect
를 사용하는 것이 좋다.
🧑🏻💻 활용 및 생각할 거리
✅ Effect에서 데이터 fetching하는 것은 좋은 방법인가?
Effect 내부에 데이터 fetching을 하는 것은 client-side 앱에서 자주 사용되지만 매우 수동적인 접근 방식이고 많은 단점이 있다.
Effects에서 직접 fetching한다는 것은 일반적으로 데이터를 미리 로드하거나 캐시하지 않는다는 의미이다. 예를 들어, 컴포넌트가 마운트를 해제했다가 다시 마운트하면 데이터를 다시 가져와야 한다. Effects에서 직접 데이터를 fetching하는 작업을 반복적으로 작성하면 나중에 캐싱 및 서버 렌더링과 같은 최적화를 추가하기가 어려워진다.
framework (Next.js)를 사용하는 경우, 프레임워크 빌트인 데이터 fetching 메커니즘을 사용하자. 그게 아니라면 React Query, useSWR, React Router 6.4+ 등과 같은 client-side 캐시를 사용하는 것을 고려하자.
✅ 데이터 Fetching 시 클린업 함수로 경쟁 조건을 피하자!
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
"hello"
를 빠르게 입력한다고 할 때,query
가"h"
에서"he"
,"hell"
,"hello"
로 점차 변경된다. 각 경우에 대해 페칭을 수행하지만, 어떤 순서로 응답이 도착할지는 보장할 수 없다. 예를 들어,"hell"
응답은"hello"
응답 이후에 도착할 수 있다. 이에 따라 마지막에 호출된setResults()
로부터 잘못된 검색 결과가 표시될 수 있다.이처럼 서로 다른 두 요청이 서로 ‘경쟁’하여 예상과 다른 순서로 도착한 경우를 ‘경쟁 조건’이라고 한다.
이를 방지하기 위해서 모든 Effect를 독립적인 프로세스로 작성하고 한 번에 하나의 셋업/클린업 주기만 생각하자. 클린업 로직이 셋업 로직을 올바르게 ‘미러링’하고 있다면, 셋업과 클린업을 자주 실행하더라도 Effect 동작에 무리가 없다.
✅ 최상위 변수로 앱 로드 당 한 번만 Effect 실행하는 방법은?
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// ✅ 앱 로드당 한 번만 실행됨
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
}
- 일부 로직이 컴포넌트 마운트 당 한번이 아니라 앱 로드당 한번 실행되야 하는 경우, 최상위 변수를 추가하여 이미 실행되었는 지 여부를 추적해야 한다.
✅ Effect의 이전 state를 기반으로 state 업데이트 하려면?
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // 매 초마다 count 업데이트
}, 1000)
return () => clearInterval(intervalId);
}, [count]);
// ...
}
count
는 반응형 값이므로 의존성 목록에 지정되어야 한다. 다만 이로 인해 state가 변경될 때마다 무한반복으로 Effect를 다시 클린업하고 셋업 하게 된다.이 문제를 해결하려면
setCount
에c => c + 1
처럼 state 업데이터 함수를 전달하는 것이 좋다.
✅ 각 이벤트 별 로직의 경우 Effect 내부에 한 번에 사용하지 말자.
- ‘버튼 클릭’ 혹은 ‘무언가 완료 시 alert 창 오픈’과 같이 상태를 계속 추적해야 하는 사용자 이벤트를 처리해야 할 경우가 있다. 이럴 경우 Effect를 사용하여 동작을 한 번에 관리하는 것 보다, 이벤트를 handler 함수에서 따로 따로 처리하는 것이 좋다. useEffect를 사용하면 값의 상태를 알 수 없기 때문에, 사용자가 어떤 동작을 했는 지 알 수 없다.
// Bad
function ProductPage({ product, addToCart }) {
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the shopping cart!`);
}
}, [product]);
function handleBuyClick() {
addToCart(product);
}
function handleCheckoutClick() {
addToCart(product);
navigateTo('/checkout');
}
// ...
}
// Good
function ProductPage({ product, addToCart }) {
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to the shopping cart!`);
}
function handleBuyClick() {
buyProduct();
}
function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}